/*
 * Copyright (C) 2010-2011 Comcast Cable.  All Rights Reserved.
 */
package com.hercules.android.receiver.handlers.commands.call;

import com.hercules.android.receiver.AndroidReceiver;
import com.hercules.android.receiver.handlers.commands.XRECommandHandlerException;
import com.hercules.android.receiver.handlers.commands.XREObjectCommandHandler;
import com.hercules.android.receiver.handlers.commands.XREViewFinderUtilityException;
import com.hercules.android.receiver.handlers.commands.call.animation.XREAnimationCallCommandHandler;
import com.hercules.android.receiver.handlers.events.EventRaiser;
import com.hercules.android.receiver.handlers.events.EventRaisingException;
import com.hercules.android.receiver.handlers.events.XREGenerateEventCallCommandHandler;
import com.hercules.android.receiver.model.commands.XRECallCommand;
import com.hercules.android.receiver.model.commands.XRECommand;
import com.hercules.android.receiver.model.events.XREEventPhase;
import com.hercules.android.receiver.model.events.XREResourceMeasuredEvent;
import com.hercules.android.receiver.model.events.XREVideoStatusEvent;
import com.hercules.android.receiver.model.objects.XREObject;
import com.hercules.android.receiver.model.objects.XREView;
import com.hercules.android.receiver.painting.XREObjectPainter;
import com.hercules.android.receiver.painting.XREObjectPainterException;
import com.hercules.android.receiver.painting.views.AndroidXREView;

import java.util.Iterator;
import java.util.List;
import java.util.Map;

/**
 * This class represents the call command handler for a XREView object. It extends from XREObjectCommandHandler. It uses
 * a XREAnimationCallCommandHandler to execute an animation. It also uses a XREGenerateEventCallCommandHandler object to
 * generate events on client side. It also uses an XREViewFinderUtility implementation to find the views affected by the
 * command.
 * <p>
 * <b>Sample Usage:</b>
 *
 * <pre>
 * // create XREViewCallCommandHandler instance
 * XREViewCallCommandHandler handler = new XREViewCallCommandHandler();
 *
 * // set required fields
 * Map&lt;String, XREObjectPainter&gt; painters = new HashMap&lt;String, XREObjectPainter&gt;();
 * painters.put(&quot;XREView&quot;, new MockPainter());
 * painters.put(&quot;XREVideo&quot;, new MockPainter());
 * handler.setObjectPainters(painters);
 * handler.setEventRaiser(new MockEventRaiser());
 * handler.setEventCallCommandHandler(new MockGenerateHandler());
 * handler.setAnimationCallCommandHandler(new MockAnimationHandler());
 * handler.setViewFinderUtility(new XRECallCommandViewFinderUtility());
 *
 * // prepare receiver
 * AndroidReceiver receiver = prepareAggregateReceiver(new XREView());
 *
 * // add child at position 1
 * XRECallCommand command = prepareCommand(&quot;addChild&quot;, CHILD_ID, 1);
 * handler.processCommandMessage(command, receiver);
 *
 * // set child index to be the last one
 * command = prepareCommand(&quot;setChildIndex&quot;, CHILD_ID, -1);
 * handler.processCommandMessage(command, receiver);
 *
 * // remove child
 * command = prepareCommand(&quot;removeChild&quot;, CHILD_ID);
 * handler.processCommandMessage(command, receiver);
 *
 * // activate view
 * command = prepareCommand(&quot;activate&quot;);
 * handler.processCommandMessage(command, receiver);
 *
 * // animate view
 * command = prepareCommand(&quot;animate&quot;);
 * handler.processCommandMessage(command, receiver);
 *
 * // generate event for the view
 * command = prepareCommand(&quot;generateEvent&quot;);
 * handler.processCommandMessage(command, receiver);
 *
 * // notify about change of view dimensions
 * command = prepareCommand(&quot;measureResource&quot;);
 * handler.processCommandMessage(command, receiver);
 * </pre>
 * <p>
 * <b>Thread Safety:</b> Not thread safe since it is mutable.
 *
 * @author DanLazar, orial
 * @version 1.0
 */
public class XREViewCallCommandHandler extends XREObjectCommandHandler {
    /**
     * The event raiser for {@link XREVideoStatusEvent} event.
     * <p>
     * It has getter and setter. Cannot be null after set. Is used in measureResource method.
     */
    private EventRaiser eventRaiser;

    /**
     * The animation call command handler.
     * <p>
     * Has a setter and a getter. Cannot be null after the setter is called. Used in animate method.
     */
    private XREAnimationCallCommandHandler animationCallCommandHandler;

    /**
     * The event call command handler.
     * <p>
     * Has a setter and a getter. Cannot be null after the setter is called. Used in generateEvent method.
     */
    private XREGenerateEventCallCommandHandler eventCallCommandHandler;

    /**
     * Default empty constructor.
     */
    public XREViewCallCommandHandler() {
        // empty
    }

    /**
     * Gets the animation call command handler.
     *
     * @return the animation call command handler
     */
    public XREAnimationCallCommandHandler getAnimationCallCommandHandler() {
        return animationCallCommandHandler;
    }

    /**
     * Sets the animation call command handler.
     *
     * @param animationCallCommandHandler the animation call command handler
     * @throws IllegalArgumentException if parameter is null
     */
    public void setAnimationCallCommandHandler(XREAnimationCallCommandHandler animationCallCommandHandler) {
        // check argument
        Helper.checkNotNull(animationCallCommandHandler, "animationCallCommandHandler");

        this.animationCallCommandHandler = animationCallCommandHandler;
    }

    /**
     * Gets the event call command handler.
     *
     * @return the event call command handler
     */
    public XREGenerateEventCallCommandHandler getEventCallCommandHandler() {
        return eventCallCommandHandler;
    }

    /**
     * Sets the event call command handler.
     *
     * @param eventCallCommandHandler the event call command handler
     * @throws IllegalArgumentException if parameter is null
     */
    public void setEventCallCommandHandler(XREGenerateEventCallCommandHandler eventCallCommandHandler) {
        // check argument
        Helper.checkNotNull(eventCallCommandHandler, "eventCallCommandHandler");

        this.eventCallCommandHandler = eventCallCommandHandler;
    }

    /**
     * Gets the event raiser for {@link XREVideoStatusEvent} event.
     *
     * @return the event raiser for {@link XREVideoStatusEvent} event
     */
    public EventRaiser getEventRaiser() {
        return eventRaiser;
    }

    /**
     * Sets the event raiser for {@link XREVideoStatusEvent} event.
     *
     * @param eventRaiser the event raiser for {@link XREVideoStatusEvent} event
     * @throws IllegalArgumentException if parameter is null
     */
    public void setEventRaiser(EventRaiser eventRaiser) {
        // check argument
        Helper.checkNotNull(eventRaiser, "eventRaiser");

        this.eventRaiser = eventRaiser;
    }

    /**
     * Process the given command.
     *
     * @param command the command
     * @param receiver the receiver
     * @throws IllegalArgumentException if any parameter is null
     * @throws IllegalStateException if required fields are not set
     * @throws XREViewCallCommandHandlerException if any error occurs while processing the command
     */
    @SuppressWarnings("boxing")
    @Override
    public void processCommandMessage(XRECommand command, AndroidReceiver receiver)
        throws XREViewCallCommandHandlerException {
        String methodName = "processCommandMessage";
        Helper.logEnter(getLogger(), this, methodName, command, receiver);

        try {
            // check arguments and get target object
            XRECallCommand callCommand = Helper.checkCommand(command);
            XREView xreView = Helper.checkReceiver(receiver, callCommand, XREView.class);

            // get command method
            String method = callCommand.getMethod();

            if ("addChild".equals(method)) {
                // get command parameters
                Integer childId = Helper.getParam(callCommand, 0, Integer.class);
                Integer index =
                    callCommand.getParams().size() == 1 ? -1 : Helper.getParam(callCommand, 1, Integer.class);

                // add child
                addChild(receiver, xreView, childId, index);
            } else if ("removeChild".equals(method)) {
                // get command parameters
                Integer childId = Helper.getParam(callCommand, 0, Integer.class);

                // remove child
                removeChild(xreView, childId);
            } else if ("setChildIndex".equals(method)) {
                // get command parameters
                Integer childId = Helper.getParam(callCommand, 0, Integer.class);
                Integer index = Helper.getParam(callCommand, 1, Integer.class);

                // set child index
                setChildIndex(xreView, childId, index);
            } else if ("animate".equals(method)) {
                animate(receiver, command);
            } else if ("activate".equals(method)) {
                activate(receiver, xreView);
            } else if ("generateEvent".equals(method)) {
                generateEvent(receiver, command);
            } else if ("measureResource".equals(method)) {
                measureResource(receiver, xreView);
            } else {
                throw new XREViewCallCommandHandlerException("Call command method '" + method + "' is not supported.");
            }

            paintAffectedViews(command, receiver, method);

            Helper.logExit(getLogger(), this, methodName);
        } catch (IllegalArgumentException e) {
            Helper.logError(getLogger(), e, methodName, e);
        } catch (IllegalStateException e) {
            Helper.logError(getLogger(), e, methodName, e);
        } catch (XREViewCallCommandHandlerException e) {
            Helper.logError(getLogger(), e, methodName, e);
        }
    }

    /**
     * Gets and paints affected views.
     *
     * @param command the call command
     * @param receiver the android receiver
     * @param method the command method
     * @throws IllegalStateException if objectPainters or viewFinderUtility field is not set
     * @throws XREViewCallCommandHandlerException if error occurs
     */
    private void paintAffectedViews(XRECommand command, AndroidReceiver receiver, String method)
        throws XREViewCallCommandHandlerException {
        if (Helper.XREVIEW_MOD_METHODS.contains(method)) {
            Map<String, XREObjectPainter> painters = getObjectPainters();

            // check state
            Helper.checkState(painters, "objectPainters");
            Helper.checkState(getAffectedViewFinder(), "affectedViewFinder");

            try {
                // find affected views
                List<XREView> affectedViews = getAffectedViewFinder().getAffectedViews(command, receiver);

                for (XREView affectedView : affectedViews) {
                    // get object, that will be painted
                    XREObject object = affectedView.getResource() == null ? affectedView : affectedView.getResource();

                    // get painter
                    String painterName = object.getClass().getSimpleName();
                    XREObjectPainter painter = painters.get(painterName);
                    if (painter == null) {
                        throw new XREViewCallCommandHandlerException("Failed to find XREObjectPainter for '"
                            + painterName + "'.");
                    }
                    painter.paintView(receiver, affectedView, command);
                }
            } catch (XREObjectPainterException e) {
                throw new XREViewCallCommandHandlerException("Error occurred while painting affected views.", e);
            } catch (XREViewFinderUtilityException e) {
                throw new XREViewCallCommandHandlerException("Error occurred while finding affected views.", e);
            }
        }
    }

    /**
     * Adds a child with the specified ID at the specified index in this view's list of children. If the index parameter
     * is omitted (if it is negative) the child is added at the end.
     *
     * @param receiver the receiver
     * @param view the xre view where the child will be added
     * @param childId the child id
     * @param index the index
     * @throws IllegalArgumentException if receiver not correct or if childId &lt; 0 or if index is greater than
     * children size
     */
    @SuppressWarnings("boxing")
    private static void addChild(AndroidReceiver receiver, XREView view, int childId, int index) {
        // check argument
        checkChildId(childId);

        // get the view identified by childId
        XREView childView =
            Helper.getObject(receiver.getApplicationData().getElementsXREIdToObject().get(childId),
                "receiver.applicationData.elementsXREIdToObject[" + childId + "]", XREView.class);

        addChild(view, index, childView);
    }

    /**
     * Removes the child with the specified ID from the view's list of children.
     *
     * @param view the xre view from which the child will be removed
     * @param childId the child id
     * @throws IllegalArgumentException if childId &lt; 0
     * @throws XREViewCallCommandHandlerException if child view with given id is not found
     */
    private static void removeChild(XREView view, int childId) throws XREViewCallCommandHandlerException {
        // check argument
        checkChildId(childId);

        // remove child by ID
        for (Iterator<XREView> i = view.getChildren().iterator(); i.hasNext();) {
            if (i.next().getId() == childId) {
                i.remove();
                return;
            }
        }

        // view is not found by id
        throw new XREViewCallCommandHandlerException("Child view with ID '" + childId + "' is not found.");
    }

    /**
     * Moves a child to the specified index in this views list of children.
     *
     * @param view the xre view where the child will be moved
     * @param childId the child id
     * @param index the index
     * @throws IllegalArgumentException if childId &lt; 0 or if index is greater than children size
     * @throws XREViewCallCommandHandlerException if child view with given id is not found
     */
    private static void setChildIndex(XREView view, int childId, int index) throws XREViewCallCommandHandlerException {
        // check arguments
        checkChildId(childId);

        // remove child by ID
        int oldIndex = 0;
        for (XREView childView : view.getChildren()) {
            if (childView.getId() == childId) {
                // remove to add at new position
                view.getChildren().remove(childView);

                // add child at new position, if adding after old index, we have to correct insertion point
                addChild(view, index, childView);
                return;
            }

            oldIndex++;
        }

        // view is not found by id
        throw new XREViewCallCommandHandlerException("Child view with ID '" + childId + "' is not found.");
    }

    /**
     * Makes the view the application's active view.
     *
     * @param receiver the receiver
     * @param view the xre view where the child will be added
     * @throws IllegalArgumentException if receiver is not correct
     */
    private static void activate(final AndroidReceiver receiver, XREView view) {
        // request focus
        final AndroidXREView androidView = getAndroidView(receiver, view.getId());

        androidView.requestFocus();
        
        receiver.getLocalBehaviorData().setFocusedView(androidView);
        receiver.getLocalBehaviorData().setFocusedReceiver(receiver);
        receiver.getLocalBehaviorData().setFocusedXREView(androidView.getXREView());
    }

    /**
     * Applies the animation by executing the given command.
     *
     * @param receiver the receiver
     * @param command the command
     * @throws IllegalStateException if required fields are not set
     * @throws XREViewCallCommandHandlerException if any other error occurs
     */
    private void animate(AndroidReceiver receiver, XRECommand command) throws XREViewCallCommandHandlerException {
        // check state
        Helper.checkState(animationCallCommandHandler, "animationCallCommandHandler");

        try {
            // process with animation command handler
            animationCallCommandHandler.onCommandMessage(command, receiver);
        } catch (XRECommandHandlerException e) {
            throw new XREViewCallCommandHandlerException(
                "Error occurred while processing command with 'animate' method by corresponding command handler.", e);
        }
    }

    /**
     * This method will generate an event on the client side.
     *
     * @param receiver the receiver
     * @param command the command
     * @throws IllegalStateException if required fields are not set
     * @throws XREViewCallCommandHandlerException if any other error occurs
     */
    private void generateEvent(AndroidReceiver receiver, XRECommand command) throws XREViewCallCommandHandlerException {
        // check state
        Helper.checkState(eventCallCommandHandler, "eventCallCommandHandler");

        try {
            // process with generate event command handler
            eventCallCommandHandler.onCommandMessage(command, receiver);
        } catch (XRECommandHandlerException e) {
            throw new XREViewCallCommandHandlerException("Error occurred while processing command "
                + "with 'generateEvent' method by corresponding command handler.", e);
        }
    }

    /**
     * Instructs the view to generate an onResourceMeasured event containing the resource's native dimensions.
     *
     * @param receiver the receiver
     * @param view the xre view
     * @throws IllegalStateException if required fields are not set
     * @throws XREViewCallCommandHandlerException if any other error occurs
     */
    private void measureResource(AndroidReceiver receiver, XREView view) throws XREViewCallCommandHandlerException {
        // check state
        Helper.checkState(eventRaiser, "eventRaiser");

        try {
            // get corresponding android view
            AndroidXREView androidView = getAndroidView(receiver, view.getId());
            int height = androidView.getResourceMeasuredHeight();
            int width = androidView.getResourceMeasuredWidth();

            // create event
            XREResourceMeasuredEvent event = new XREResourceMeasuredEvent();
            event.setName(XREResourceMeasuredEvent.ON_RESOURCE_MEASURED_EVENT_NAME);
            event.setHeight(height);
            event.setWidth(width);
            event.setX(0);
            event.setY(0);
            event.setSource(view.getId());
            event.setHandler(view.getId());
            event.setPhase(XREEventPhase.STANDARD);

            // raise event
            eventRaiser.raiseEvent(event, receiver, false);
        } catch (EventRaisingException e) {
            throw new XREViewCallCommandHandlerException("Error occurred while raising 'onResourceMeasured' event.", e);
        }
    }

    /**
     * Checks child ID command parameter.
     *
     * @param childId the child ID value
     * @throws IllegalArgumentException if childId is negative
     */
    private static void checkChildId(int childId) {
        if (childId < 0) {
            throw new IllegalArgumentException("Value of 'childId', command parameter #0, should be not negative.");
        }
    }

    /**
     * Adds child to view at given index.
     *
     * @param view the view
     * @param index the position of child in view's children
     * @param childView the child, that will be added to view
     * @throws IllegalArgumentException if index is greater than children size
     */
    private static void addChild(XREView view, int index, XREView childView) {
        // check index
        if (index > view.getChildren().size()) {
            throw new IllegalArgumentException("Value of 'index' command parameter #1 (" + index
                + ") is greater than view children size (" + view.getChildren().size() + ").");
        }

        if (index >= 0) {
            // add child at specified index
            view.getChildren().add(index, childView);
        } else {
            // add child to the end
            view.getChildren().add(childView);
        }
        
        childView.setParent(view);
    }

    /**
     * Gets corresponding android view.
     *
     * @param receiver the android receiver
     * @param viewId the id of required view
     * @return the corresponding android view
     * @throws IllegalArgumentException if receiver is not correct or view is not found
     */
    @SuppressWarnings("boxing")
    private static AndroidXREView getAndroidView(AndroidReceiver receiver, int viewId) {
        Helper.checkNotNull(receiver.getApplicationData().getViewsXREIdToView(),
            "receiver.applicationData.viewsXREIdToView");

        // get corresponding android view
        AndroidXREView androidView = receiver.getApplicationData().getViewsXREIdToView().get(viewId);

        // check if view is present
        Helper.checkNotNull(androidView, "receiver.applicationData.viewsXREIdToView[" + viewId + "]");
        return androidView;
    }

}